package daemon;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.*;
import java.io.*;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import common.*;

/**
 * Represents a remote directory with an interface to work on files and subdirectories.
 * This is the core class in every client instance. It gathers the implementations of 
 * all functionalities that longcat provides. Methods from this class are invoked from
 * the main function which is a method of @see {@link daemon.LongcatDaemon#getClass()}.
 * Moreover the Storage Class exploits a PackageServer instance used to handle package
 * facilitation.
 */
public class Storage extends UnicastRemoteObject implements StorageInterface    {
	/**
	 * The serial version unique identifier (SUID) of a class is a 64-bit hashcode computed from all the
	 * class's properties: its classname, the names of all interfaces which it implements, and the names
	 * and signatures of its member functions and fields. The SUID is identical for all versions of a class
	 * which are compatible under serialization.
	 * The serial version unique identifier (SUID) of a class is a 64-bit hashcode computed from all the
	 * class's properties: its classname, the names of all interfaces which it implements, and the names
	 * and signatures of its member functions and fields. The SUID is identical for all versions of a class
	 * which are compatible under serialization.
	 */
	private static final long serialVersionUID = 4933267315618732914L;
	/**
	 * A reference to a PackageServer object. 
	 * PackageServer holds the function of package handling.
	 * Each Storage instance is supposed to employ a slave - PackageServer instance. 
	 */
	private PackageServer packageServer;
	/**
	 * A path to a directory used to store anonymous packages as a part of a cloud.
	 * 
	 */
	private String packagePath;
	private LongcatServerInterface server;
	/**
	 * TODO:comment
	 * @param server
	 * @param packagePath
	 */

	public Storage(LongcatServerInterface server, String packagePath, int packageServerPort) 
			throws RemoteException{
		packageServer = new PackageServer(packageServerPort, packagePath);
		this.packagePath = packagePath;
		this.server = server;
		server.registerStorage(this, packageServer.getPort());
	}

	
	/**
	 * Function implementing a distributed deletion of a file.
	 * @param remotePath file to be deleted from a cloud
	 */
	public void delete(String remotePath){
		try {
			server.delete(remotePath);
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}


	/** 
	 * Simply removes package from a storage.
	 * @param ID id of a package to be removed
	 */
	public void removePackage(PackageID ID){
		File dir = new File(packagePath);
		String[] files = dir.list();
		for(String file: files){
			File currentFile = new File(file);
			if(currentFile.getName().equals(ID.toString()))
				currentFile.delete();
		}
	}
	/**
	 * Disconnecting from distributed storage. Files which were
	 * stored on disconnecting machine are regenerated from copies remaining in cloud. 
	 */
	public void disconnect(){
		try {
			server.disconnect(this);
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	/**
	 * Lists collection of files in the distributed directory.  
	 */
	public void list(){
		ServerListResult list;
		try {
			list = server.list();
			Iterator<String> itr = list.listOfFiles.iterator();
			System.out.println("List of files:");
			while(itr.hasNext()){
				System.out.println(itr.next());
			}
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	/**
	 * Downloads a file and writes it to a specified location.
	 * Blocks until the file is ready or an error is thrown.
	 * @param remotePath a path relative to the root of distributed storage, pointing at the file to be downloaded
	 * @param localPath a local path, to which the file should be written
	 * @throws IOException 
	 */
	public void get(String remotePath, String localPath) throws IOException {
	
		ServerGetResult result = server.get(remotePath);
		
		//For now function downloads all needed packages into package directory but doesn't register them
		//then concatenate function creates file and saves it to specific location
		//TODO: delete no longer necessary packages
		for(Map.Entry<Integer, Collection<NetLocation>> entry:result.partLocations.entrySet()){
			NetLocation randomedLocation = entry.getValue().iterator().next();
			obtainPackage(new PackageID(result.fileID, entry.getKey()), randomedLocation);
			//TODO: location not random yet, for now the first element is chosen
		}
		concat(result.fileID, localPath);
	}
	
	

	/**
	 * Implements file concatenation. After a successful download of necessary packages
	 * packages with common fileID are sorted and then tied up to a complete file.
	 * @param id file's id
	 * @param filePath determines target location
	 * @throws IOException
	 */
	public void concat(int id, String filePath) throws IOException{
        File file = new File(packagePath+"/");
        File[] files = file.listFiles();
        Arrays.sort(files, new PackagesSort());
        InputStream in;
        OutputStream out = new FileOutputStream(filePath);
        for (int i = 0; i < files.length; i++) {
        		if(files[i].getName().equals(Integer.toString(id)))
        			continue;
        		in = new FileInputStream(files[i]);
        		System.out.println(files[i].getName());
        		byte[] buf = new byte[8192];
        		int len = in.read(buf);
                while (len >0) {
                	out.write(buf, 0, len);
                	len = in.read(buf);
                }
        }
	}
	
	
	/**
	 * Opposing to concat function. Splits a file into packages of specified size. 
	 * @param filePath file to be splitted
	 * @param id number specific to a file
	 * @param size a size of a package
	 * @throws IOException
	 */
	public void fileSplitter(String filePath, int id, int size) throws IOException{
		FileInputStream fis = new  FileInputStream(filePath);
		byte buffer[] = new byte[size];
	    int count = 1;
	    
	    while (true) {
	      int i = fis.read(buffer, 0, size);
	      if (i == -1)
	        break;
	      String filename = packagePath + "/"+ Integer.toString(id)+"." + count;
	      FileOutputStream fos = new FileOutputStream(filename);
	      fos.write(buffer, 0, i);
	      fos.flush();
	      fos.close();
	      packageServer.registerPackage(new PackageID(id, count),filename);
	      ++count;
	    }

		
	}

	/**
	 * Stores a local file in the storage. 
	 * Blocks until the upload is complete or an error is thrown.
	 * If the file does not exist in storage, it is created. Otherwise, the one in storage is overwritten.
	 * @param localPath a local path, pointing at the file to be uploaded
	 * @param remotePath a path relative to the root of user's storage, to which the file should be written
	 * @throws IOException 
	 */	
	public void store(String localPath, String remotePath) throws Exception {
		ServerStoreResult serverResult = server.store(remotePath, new File(localPath).length());
		fileSplitter(localPath,serverResult.ID, serverResult.packageSize);
		//Client runs store() on server interface DONE
		//Client receives packageCount and package id as function result DONE
		//Splitting the file into packageCount parts DONE

		//TODO: calls OK (server, interface)

		server.ok(this, remotePath);
	}
	
	/**
	 * Downloads a package from a <class>PackageServer</class>
	 */
	@Override
	public void obtainPackage(PackageID ID, NetLocation location) {
		try {
		    String id = Integer.toString(ID.fileID) +"."+ Integer.toString(ID.partID);
		    File file = new File(packagePath +"/"+ id);
			System.out.println("obtaining package " + ID + " from " + location + " meaning " + packagePath +"/"+ id);
		    if(file.exists()) return;
			Socket socket = new Socket(location.address, location.port);
		    OutputStream os = socket.getOutputStream();
		    InputStream in = socket.getInputStream();
		    ByteStream.toStream(os, id);
		    ByteStream.toFile(in, file);
		} catch (IOException e) {
			e.printStackTrace();
		}
		//throw new UnsupportedOperationException();		
	}

	@Override
	public void noop() throws RemoteException {
		System.out.println("NOOP");
	}
}
